package nullInvader.Control;

import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.sql.SQLException;
import java.util.ArrayList;

import javax.swing.JOptionPane;
import javax.swing.Timer;

import nullInvader.Engine.DatabaseHandler;
import nullInvader.Engine.MapManager;
import nullInvader.Engine.Player;
import nullInvader.Engine.UnitInfo;
import nullInvader.View.Gui;

/**
 * This class is used as the main controller for the game. It enables
 * interaction between the two subsystems View and Model. These are
 * more specifically the GUI related classes and the game logic such
 * as the Player class. It attaches smaller controller
 * classes(anonymous ActionListeners) to the GUI and captures events
 * being triggered on it. Many of these events involve specific
 * interaction with the game Model. This controller allows for the
 * translation of GUI interaction to Model reaction.The result of the
 * detachment between the View and Model. Is a more dynamic system in
 * which a GUI/Model subsystem can be completely restructured and no
 * changes to the other subsystem should be required.
 * 
 * @author Emanuel Yifter Lindgren (dv11elr)
 * @version 1.0
 */
public class GameControl {

    /* how often the Units move. default 1000ms */
    private static final int UNIT_MOVE_INTERVAL = 1000;
    /* how often the drawtimer is fired off. default 1/24s */
    private static final int FRAMES_PER_SECOND = 24;
    private static final int ICON_SIZE = 20;

    private MapManager mapManager;
    private Player player;
    private Gui gui;
    private DatabaseHandler dbHandler;
    private Timer drawTimer;
    private final GameControl controller = this;

    private volatile boolean paused = false;
    private boolean started = false;

    private int currentMapHighscore = -1;
    private final String mapPath;

    /**
     * Creates an instance of this class. If the parameter mapPath is
     * null. The default map "levels.xml" will be used.
     * 
     * @param mapPath
     *            a path to a map file. This value can be null.
     */
    public GameControl(String mapPath) {

        this.mapPath = mapPath;

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    gui = new Gui(controller);
                    attachGUIActionListeners();
                    attachGUIMouseAdapters();
                } catch (Exception e) {
                    System.out.println("Failed to initialize GUI."
                            + " Can't continue. " + e.getMessage());
                    System.exit(0);
                }

                try {
                    startDrawTimer();
                    gui.setVisible(true);
                } catch (Exception e) {
                    System.out.println("Failed to start the draw timer."
                            + " Can't continue. " + e.getMessage());
                    System.exit(0);
                }
            }
        });

    }

    /*
     * starts the Swing Timer which is responsible for drawing to the
     * map component on the GUI.
     */
    private void startDrawTimer() {
        drawTimer = new Timer(1000 / FRAMES_PER_SECOND, new DrawMapListener(
                controller, gui, ICON_SIZE));
    }

    private void attachGUIMouseAdapters() {
        /*
         * Below MouseAdapter captures clicks on the map component and
         * tells the object(unit/brick)that it was clicked. If there's
         * no unit on the coordinate the brick will be told not both.
         * This is to prevent click-through.
         */
        gui.addMapListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                if (!started || paused) {
                    return;
                }
                Point gridPoint = convertToInternalGridPoint(e.getPoint());

                try {
                    mapManager.getMap().getZone(gridPoint).getUnitList()
                            .get(0).interact();
                    /*
                     * imho ugly race condition fix.
                     */
                } catch (IndexOutOfBoundsException e1) {
                    mapManager.getMap().getZone(gridPoint).getBrick()
                            .wasClicked();
                } catch (NullPointerException e1) {
                    /* nothing existed at the point. */
                    return;
                }

            }

        });
        /*
         * Below MouseAdapter tells the Player object to buy a
         * "Knight" unit.
         */
        gui.addUnit1Listener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                if (!started || paused) {
                    return;
                }
                requestUnit("Knight");
            }
        });

        /*
         * Below MouseAdapter tells the Player object to buy a
         * "Teleporter" unit.
         */
        gui.addUnit2Listener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                if (!started || paused) {
                    return;
                }
                requestUnit("Teleporter");
            }

        });
    }

    /*
     * Attaches all the relevant Actionlisteners to the GUI. These all
     * handle their respective event.
     */
    private void attachGUIActionListeners() {

        gui.addGameStartListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                startGame();
            }

        });

        /*
         * This actionlistener is attached to the component being used
         * when the player wants to toggle the paused state in the
         * game.
         */
        gui.addMapPauseListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (!started) {
                    return;
                }
                if (paused) {
                    drawTimer.start();
                    paused = true;
                } else {
                    drawTimer.stop();
                    paused = false;
                }
                gui.setPausedState(paused);
                paused = !paused;
            }

        });
        /*
         * This actionlistener is attached to the component being used
         * if the player wants to restart the map.
         */
        gui.addMapRestartListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (!started || paused) {
                    return;
                }
                restartMap();

            }

        });
        /*
         * This actionlistener is attached to the component being used
         * if the player wants to quit the game.
         */
        gui.addGameQuitListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                quitGame();
            }

        });

        /*
         * Below actionlistener opens a dialogbox with information
         * about the game when the player clicks a gui component.
         */
        gui.addHelpAboutListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(gui,
                        "NullInvader was created by:"
                                + "\nEmanuel Yifter Lindgren."
                                + "\nRobert Lundström." + "\nFredrik Loch."
                                + "\nMaria Gramner.", "About",
                        JOptionPane.PLAIN_MESSAGE);
            }

        });
        /*
         * Below actionlistener opens a dialogbox with control
         * information when the player clicks a gui component.
         */
        gui.addHelpControlsListener(new ActionListener() {

            private final String controlsMsg = "The controls are simple.\n"
                    + " Buy units by clicking their picture on the sidemenu.\n"
                    + " The goal is to get enough units to the castle.\n"
                    + " For a higher score, use as few units as possible.\n";

            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(gui, controlsMsg, "Controls",
                        JOptionPane.PLAIN_MESSAGE);
            }

        });
    }

    /*
     * Start or Restarts the whole game depending on whether it was
     * previously started or not ( which means we must clean up ).
     */
    private void startGame() {
        if (started) {
            stopDrawAndKillPlayer();
        }

        /*
         * started is tested to prevent multiple connect tries to an
         * offline db server.
         */
        if (dbHandler == null && !started) {
            try {
                dbHandler = new DatabaseHandler();
            } catch (ClassNotFoundException e) {
                dbHandler = null;
                gui.promptErrorDialog(e.getMessage(), false);
            }
        }
        /* if user supplied an alternative map file at command-line. */
        try {
            if (mapPath != null) {

                mapManager = new MapManager(mapPath);
            } else {
                mapManager = new MapManager();
            }
            player = new Player(mapManager.getMap(), UNIT_MOVE_INTERVAL,
                    controller);
            new Thread(player).start();
            gui.setStartedState();
            drawTimer.start();
            started = true;
            paused = false;

            setUnitTooltips();
            updateHighscore();
        } catch (Exception e) {
            /*
             * The MapManager could not be created. Nothing to be done
             * but exit.
             */
            gui.promptErrorDialog(e.getMessage(), true);
        }

    }

    /**
     * Simply quits the game. Cleanup should be made here if
     * necessary.
     */
    public void quitGame() {
        System.exit(0);
    }

    /* Restarts a running map/level. */
    private void restartMap() {

        stopDrawAndKillPlayer();

        try {
            mapManager.resetCurrentMap();
        } catch (Exception e) {
            gui.promptErrorDialog(e.getMessage(), true);
        }

        player = new Player(mapManager.getMap(), UNIT_MOVE_INTERVAL,
                controller);
        new Thread(player).start();
        drawTimer.start();

        updateHighscore();
        paused = false;
    }

    /* Starts the next map. */
    private void runNextMap() {
        stopDrawAndKillPlayer();
        try {
            player = new Player(mapManager.getNextMap(), UNIT_MOVE_INTERVAL,
                    controller);
            new Thread(player).start();
            drawTimer.start();
            paused = false;
            setUnitTooltips();
            updateHighscore();
        } catch (IndexOutOfBoundsException e) {
            gui.promptGameFinishedDialog();
        }

    }

    /*
     * Helper method to prevent duplicate code, used by other methods
     * that need to stop the drawing timer and kill the player thread.
     */
    private void stopDrawAndKillPlayer() {
        if (player != null) {
            drawTimer.stop();
            player.kill();
        }
    }

    /*
     * Converts a click-coordinate received from the map component
     * into its representation in the internal HashMap grid.
     */
    private Point convertToInternalGridPoint(Point extPoint) {

        extPoint.setLocation((int) ((extPoint.getX() / ICON_SIZE)),
                (int) ((extPoint.getY() / ICON_SIZE)));

        return extPoint;
    }

    /*
     * This method just updates the GUI display of current Cash and
     * Score
     */
    void updatePlayerStats() {
        if (player != null) {
            gui.updateStats(player.getCash(), player.getScore(), mapManager
                    .getMap().getGoalScore());

        }
    }

    /*
     * Retrieves the highscore for the loaded map and submits the info
     * to the GUI for display.
     */
    private void updateHighscore() {
        if (dbHandler != null && dbHandler.getConnectionStatus()) {
            String[] info = dbHandler.getHighscore(mapManager.getMap()
                    .getName());
            /* max 10 char nicknames. */
            if (info[0].length() > 10) {
                info[0] = info[0].substring(0, 10);
            }
            gui.setHighscore(info[1], info[0]);
            currentMapHighscore = Integer.parseInt(info[1]);
        }
    }

    /*
     * Retrieves Unit info from the MapManager and submits the info to
     * the GUI for display.
     */
    private void setUnitTooltips() {

        if (mapManager != null) {
            ArrayList<UnitInfo> unitInfo = mapManager.getMap().getUnitPrices();
            if (unitInfo != null && unitInfo.size() == 2) {

                gui.setUnitTooltip(unitInfo.get(0).getName(), unitInfo.get(1)
                        .getName(), unitInfo.get(0).getPrice(), unitInfo
                        .get(1).getPrice());
            }
        }
    }

    /*
     * Sends a request to buy a new Unit(identified by its name)from
     * the game logic(Player).
     */
    private void requestUnit(String unitName) {
        if (player != null) {
            if (!player.buyUnit(unitName)) {
                gui.promptErrorDialog(
                        "You cant afford to buy the " + unitName, false);
            }
        }
    }

    /**
     * Either restarts the game or quits the game depending on the
     * choice parameter. This method is used to react upon user input
     * from the GUI.
     * 
     * @param choice
     *            The choice received from the user. True to restart
     *            the game. False to quit it.
     */
    public void lostResponse(final boolean choice) {
        if (choice) {
            startGame();
        } else {
            quitGame();
        }
    }

    /**
     * A call to this method means the player lost. The player is then
     * asked which action he would like to take.
     * 
     */
    public void reportLoss() {
        paused = true;
        gui.promptLossDialog();
    }

    /**
     * Either restarts the map or runs the next map depending on the
     * choice parameter. This method is used to react upon user input
     * from the GUI.
     * 
     * @param choice
     *            The choice received from the user. True to restart
     *            the map. False to proceed to the next map.
     */
    public void winResponse(final boolean choice) {

        if (choice) {
            restartMap();
        } else {
            runNextMap();
        }

    }

    /**
     * A call to this method means the player won a map. The players
     * score is compared against the highscore database and
     * dialogboxes will be opened on the GUI for input.
     * 
     * @param score
     *            how much score the user had when he won the map.
     */
    public void reportWin(int score) {
        paused = true;
        gui.promptWinDialog();
        if (currentMapHighscore != -1 && currentMapHighscore < score) {
            gui.promptHighscoreSubmitDialog(score);
        }
    }

    /**
     * This method is used to submit a response on the question
     * whether the player wants to submit his score to the highscore
     * database. If the input is a valid nickname then the method will
     * submit the score to the db under that nick. If the nickname is
     * longer then 10 characters it will be truncated.
     * 
     * @param nick
     *            the nickname of the current player.
     * @param highscore
     *            the highscore to submit.
     */
    public void highscoreResponse(String nick, int highscore) {
        if (nick != null && nick.length() > 0 && dbHandler != null) {
            try {
                if (nick.length() > 10) {
                    /* max 10 char nicknames. */
                    nick = nick.substring(0, 10);
                }
                dbHandler.submitScore(nick, mapManager.getMap().getName(),
                        highscore);
            } catch (SQLException e) {
                dbHandler = null;
                gui.promptErrorDialog(e.getMessage(), false);
            }
        }

    }

    /**
     * Retrieves the MapManager object this GameControl is using.
     * 
     * @return the mapManager object.
     **/
    public MapManager getMapManager() {
        return mapManager;
    }

    /**
     * Returns whether the game is currently paused or not.
     * 
     * @return true if the game is paused, false if not.
     */
    public boolean isPaused() {
        return paused;
    }
}
